iT邦幫忙

2025 iThome 鐵人賽

DAY 22
0
Vue.js

邊學邊做:Vue.js 實戰養成計畫系列 第 22

Day 22:與外星文明溝通 — Axios

  • 分享至 

  • xImage
  •  

Axios 是什麼?

一個 JavaScript 函式庫,專門用來發送 HTTP 請求(像 GET、POST)到伺服器,並接收回應。可以用在 Vue、React、Node.js 等各種環境。
讓你不用自己寫一堆 XMLHttpRequest 或 fetch,語法更直覺、功能更多。

  • axios.get() → 拿資料
  • axios.post() → 傳送資料

Axios 的特色

1.語法簡單

import axios from 'axios'
axios.get('https://jsonplaceholder.typicode.com/posts')
  .then(res => console.log(res.data))

2.支援 Promise / async-await
更方便處理非同步。

const res = await axios.get('/api/data')
console.log(res.data)

只要一行就能拿到資料。

3.自動轉換 JSON
不需要手動 .json(),直接就拿到物件。

4.全域設定
可以設 baseURL、timeout、headers,一次設定全域適用。

5.攔截器
請求送出前、回應回來後可以攔截,常用來統一處理「帶 Token」或「錯誤提示」。

6.瀏覽器 & Node.js 都能用
前端後端共通。

白話理解

  • 如果把你的 App 想成一艘太空船,Axios 就是你的通訊器。
  • 你可以說:「送出一個請求到地球要資料」→ 它幫你發射訊號。
  • 當地球回資料回來,它會幫你接收,並自動把格式轉換好。
  • 你還能設定「每次發訊號都帶上身分證(token)」,或是「如果伺服器回錯誤,就自動提醒我」。

實作

安裝

npm install axios

建立一支可重用的通訊器(建議做法)
src/lib/http.js 先創建一個 axios 實例,未來所有 API 都走這裡。

import axios from 'axios'

// 從環境變數帶入後端位址(.env 用 VITE_ 前綴)
const http = axios.create({
  baseURL: import.meta.env.VITE_API_BASE || 'https://jsonplaceholder.typicode.com',
  timeout: 10000
})

// 請求攔截器:帶 token、統一 header
http.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})

// 回應攔截器:只回傳 data,錯誤統一拋出
http.interceptors.response.use(
  res => res.data,
  err => {
    // 你可以在這裡做 401 轉跳登入、彈錯誤訊息等
    return Promise.reject(err)
  }
)

export default http

.env 範例(根目錄):

VITE_API_BASE=https://jsonplaceholder.typicode.com

在 Vue 裡實作一個「外星通訊站」:src/views/CommCenter.vue
以下用 Composition API 示範列表讀取、單筆詳細、載入/錯誤狀態、重新整理與取消請求。

<template>
  <main class="wrap">
    <h1>🛰️ 外星通訊站</h1>

    <section class="toolbar">
      <button @click="fetchList" :disabled="loading">重新整理</button>
      <span v-if="loading">📡 訊號連線中…</span>
      <span v-if="error" class="err">❌ {{ error }}</span>
    </section>

    <ul class="list" v-if="messages.length">
      <li v-for="m in messages" :key="m.id" @click="open(m.id)">
        <strong>#{{ m.id }}</strong> {{ m.title }}
      </li>
    </ul>
    <p v-else-if="!loading && !error">目前沒有訊息。</p>

    <article v-if="detail" class="detail">
      <h2>訊息 #{{ detail.id }}</h2>
      <p>{{ detail.body }}</p>
      <button @click="detail = null">關閉</button>
    </article>
  </main>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import http from '../lib/http'
import axios from 'axios'

const messages = ref([])
const detail   = ref(null)
const loading  = ref(false)
const error    = ref('')

let cancel // 用於取消請求

async function fetchList() {
  error.value = ''
  detail.value = null
  loading.value = true

  // 取消上一個未完成請求(如果有)
  if (cancel) cancel('Use new request')
  const controller = new AbortController()
  cancel = (reason) => controller.abort(reason)

  try {
    // 以 JSONPlaceholder 當作示範 API
    messages.value = await http.get('/posts', { signal: controller.signal })
  } catch (err) {
    if (axios.isCancel(err)) return
    error.value = parseAxiosError(err)
  } finally {
    loading.value = false
  }
}

async function open(id) {
  error.value = ''
  detail.value = null
  loading.value = true
  const controller = new AbortController()

  try {
    detail.value = await http.get(`/posts/${id}`, { signal: controller.signal })
  } catch (err) {
    if (axios.isCancel(err)) return
    error.value = parseAxiosError(err)
  } finally {
    loading.value = false
  }
}

function parseAxiosError(err) {
  // 友善錯誤訊息(可依專案客製)
  if (err.response) {
    return `伺服器錯誤(${err.response.status})`
  } else if (err.request) {
    return '網路連線異常或伺服器無回應'
  } else {
    return err.message || '未知錯誤'
  }
}

onMounted(fetchList)
onBeforeUnmount(() => cancel && cancel('Component unmounted'))
</script>

<style scoped>
.wrap { max-width: 860px; margin: 40px auto; padding: 0 16px; font: 16px/1.6 ui-sans-serif, system-ui; }
.toolbar { display:flex; gap:12px; align-items:center; margin-bottom:12px; }
.err { color:#ef4444; }
.list { list-style:none; padding:0; }
.list li { padding:8px 10px; border:1px solid #e5e7eb; border-radius:8px; margin-bottom:8px; cursor:pointer; }
.list li:hover { background:#f8fafc; }
.detail { border:1px solid #e5e7eb; border-radius:12px; padding:12px; margin-top:12px; background:#fff; }
</style>

src/router/index.js
你可以把這頁註冊到路由中,例如 routes: [{ path: '/comm', component: CommCenter }],或放在既有頁面中引用。

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Planet from '../views/Planet.vue'

// 子頁面(相對路徑,沒有前導斜線)
import PlanetOverview from '../views/planet/Overview.vue'
import PlanetMoons from '../views/planet/Moons.vue'
import PlanetResearch from '../views/planet/Research.vue'

import NotFound from '../views/NotFound.vue'
import CommCenter from '../views/CommCenter.vue' // 外星通訊站

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', name: 'home', component: Home },

    {
      path: '/planet/:slug',
      name: 'planet',
      component: Planet,
      props: route => ({ slug: route.params.slug }),
      children: [
        { path: '', redirect: { name: 'planet-overview' } },
        { path: 'overview',  name: 'planet-overview',  component: PlanetOverview,  props: true },
        { path: 'moons',     name: 'planet-moons',     component: PlanetMoons,     props: true },
        { path: 'research',  name: 'planet-research',  component: PlanetResearch,  props: true },
        // ⚠ 這裡不要放 /comm
      ]
    },

    // ⬇️ 頂層獨立路由
    { path: '/comm', name: 'comm', component: CommCenter },

    { path: '/:pathMatch(.*)*', name: '404', component: NotFound }
  ],
  scrollBehavior: () => ({ top: 0 })
})

export default router

App.vue

<template>
  <header class="nav">
    <RouterLink to="/" class="brand">🚀 Orbit Coders</RouterLink>
    <RouterLink to="/comm">🛰️ 外星通訊站</RouterLink> 
  </header>
  <RouterView />
</template>

<style scoped>
.nav { display:flex; gap:16px; align-items:center; padding:12px 16px; border-bottom:1px solid #e2e8f0; background:#ffffff; position:sticky; top:0; z-index:10; }
.brand { text-decoration:none; font-weight:700; color:#0f172a; }
</style>

乾淨的 API 模組(可選、推薦)
專案長大後,把每個領域的 API 拆檔:src/apis/posts.js

import http from '../lib/http'

export function listPosts(params) {
  return http.get('/posts', { params })
}
export function getPost(id) {
  return http.get(`/posts/${id}`)
}
export function createPost(payload) {
  return http.post('/posts', payload)
}
export function updatePost(id, payload) {
  return http.put(`/posts/${id}`, payload)
}
export function deletePost(id) {
  return http.delete(`/posts/${id}`)
}

元件就只管呼叫 listPosts()getPost(),不碰 axios 細節 → 減少耦合、好測試。

明天我們將進一步了解Vue 與本地儲存(localStorage)!

參考資源
https://vuejs.org/guide
https://www.runoob.com/vue3


上一篇
Day 21:共享能量池 — 跨元件資料共享(store 實作)
下一篇
Day 23:星際任務日誌 — Vue 與本地儲存(localStorage)
系列文
邊學邊做:Vue.js 實戰養成計畫26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言